<?php

class GXSimpleXML {

	// the DOMDocument
	public $doc;

	// version
	private $version = '1.0';
	// encoding
	private $encoding = 'UTF-8';

	// the node created last
	private $lastNode;

	public function __construct( $in = null, $value = null, $attributes = null ){
		// create the DOM document
		//$this->doc = simplexml_import_dom( new DOMDocument( $this->version, $this->encoding ) );

		// handle @in
		switch(true){
			// if it's an object...
			case is_object($in):
				$this->doc = new GXSimpleXMLElement( $this->doc, get_class($in), $in );
				break;

			// if it's an array ... have to put this case here since php5.2 will bork on the regex in next case if it's an array
			case is_array($in):
				$this->doc = new GXSimpleXMLElement( $this->doc, key($in), $in );
				break;

			// if it's a path ... unix or windows ... or if the provided string points to a file
			case substr($in,0,1) == DIRECTORY_SEPARATOR || preg_match('/^[a-zA-Z]{1}:/',$in) || is_file($in):
				if( !$this->doc = simplexml_load_file($in) ){
					trigger_error( 'Unable to load file: ' .$in, E_USER_ERROR );
					return false;
				}
				break;

			// if it looks like markup: first character must be "<" and last character must be ">"
			case substr(trim($in),0,1) == '<' && substr(trim($in),strlen(trim($in))-1,1) == '>':
				if( !$this->doc = simplexml_load_string($in) ){
					trigger_error( 'Unable to load XML string', E_USER_ERROR );
					return false;
				}
				break;

			// otherwise, treat @in as the name of the root node
			default:
				// attempt to sense value vs. attributes
				if( !$attributes && is_array($value) ){
					$attributes = $value;
					unset($value);
				}
				// create a null variable to pass by reference
				$tmp = null;
				// store
				$node = new GXSimpleXMLElement( $tmp, $in, $value, $attributes );
				$this->lastNode = $node->me;
				$this->doc = $node->doc;
		}
		return $this;
	}

	/*
	 * __call (overload method)
	 *
	 * Wraps GXSimpleXMLElement creation, allowing you to pass a function name as the tag name.
	 */
	public function __call( $method, $args ){
		if( $method != __CLASS__ ){
			// handle reserved method names
			// can't use these in your xml file
			switch( $method ){
				case 'replaceNode':
					// @doc, @replaceWith, @toReplace, @xpath
					return GXSimpleXMLTools::replaceNode( $this->doc, $args[0], $args[1], $args[2] );
					break;
				case 'setStyleSheet':
					// @doc, @href
					return GXSimpleXMLTools::setStyleSheet( $this->doc, $args[0] );
					break;
				case 'xml':
				case 'saveXML':
				case 'asXML':
					// @doc
					return GXSimpleXMLTools::xml( $this );
					break;
				default:
					$this->lastNode = new GXSimpleXMLElement( $this->doc, $method, $args[0], $args[1] );
					return $this->lastNode;
			}
		}
		trigger_error( 'Invalid method call in GXSimpleXML', E_USER_ERROR );
		return false;
	}

}

class GXSimpleXMLElement {

	// reference to the xml doc
	public $doc;
	// reference to this dom element
	public $me;
	// reference to the node we're going to use as our parent
	private $to;

	private $defaultTag = 'root';

	// if this is true, don't create a containing node for arrays and objects
	const GXDOM_SKIP_CONTAINER = true;
	// if this is true, treat :method as a method call in __call()
	const GXDOM_CALL = false;

	public function __construct( &$doc, $tag = null, $value = null, $attributes = null, &$to = null, $options = null ){
		// must have a tag
		if( !$tag || !is_string($tag) ){
			$tag = $this->defaultTag;
		}

		// sense arguments
		$skipContainer = false;
		$args = func_get_args();
		for( $i = 0; $i < count($args); $i++ ){
			if( $args[$i] == GXDOM_SKIP_CONTAINER ){
				$skipContainer = true;
			}
		}

		// assign references
		$this->doc = $doc;
		$this->to = $to;
		if( !is_object($this->to) ){
			// check for a root node
			if( $tmp = GXSimpleXMLTools::xpath($this->doc,'/*[1]') ){
				$this->to = $tmp[0];
			}else{
				$this->to = $this->doc;
			}
		}

		// sanitize name
		$nodeName = GXSimpleXMLTools::cleanTag($tag);

		// if we have a value and it's an object or array, create a holding node, but only if we're not skipping it
		if( $value && (is_object($value) || is_array($value)) && !$skipContainer ){
			// attach this new node to "to", not to "me"
			$node = $this->$nodeName(null,null,$this->to);
			// assign the attach node to the new node
			$this->to = $node->me;
		}
		if( !$node ){
			$node = $this;
		}

		// handle different types of value
		switch( gettype($value) ){

			case 'object':

				showval(get_class($value));

				// if we're attaching GXSimpleXML stuff...
				switch(true){
					// import the entire doc
					case get_class($value) == 'GXSimpleXML':
						$in = $this->doc->importNode( $value->doc, true );
						break;
					// import just the current DOMElement
					case get_class($value) == 'GXSimpleXMLElement':
						$in = $this->doc->importNode( $value->me, true );
						break;
				}
				if( $in ){
					$node->to->addChild($in);
					return $this;
				}
				// if it's not a GXSimpleXMLx object, convert it to an array and keep going
				$value = get_object_vars($value);
				//break; no break so goes directly to 'array'

			case 'array':
				// store the :to we were initially pointing to
				$holdTo = $this->to;
				foreach( $value as $key => $val ){
					// if the key is numeric, use the parent's node name, since tags cannot be numbers
					unset($att);
					if( is_numeric($key) ){
						// add the number as @id
						$att = array('id'=>$key);
						$key = $this->to->getName();
						// if there's no root node, use the default tag name
						if( !$key ){
							$key = $this->defaultTag;
						}
					}
					// use the node created above
					$node->$key($val,$att,$this->to);
					// return the enclosing node
					$this->me = $node->to;
				}
				// restore the original :to
				$this->me = $holdTo;
				break;

			default:
				if( !$this->doc ){
					$str = '<' .$nodeName .'>' .GXSimpleXMLTools::cleanText(strval($value)) .'</' .$nodeName .'>';
					$this->doc = new SimpleXMLElement( $str );
					$SimpleNode = $this->doc;
				}else{
					$SimpleNode = $this->to->addChild( $nodeName, GXSimpleXMLTools::cleanText(strval($value)) );
				}
				// attach attributes if we have them
				if( is_array($attributes) ){
					foreach( $attributes as $key => $val ){
						$SimpleNode->addAttribute( GXSimpleXMLTools::cleanTag($key), GXSimpleXMLTools::cleanText($val) );
					}
				}
				// assign latest reference
				$this->me = $SimpleNode;

		}
		// return this GXSimpleXMLElement
		return $this;
	}

	/*
	 * __call
	 *
	 * Treats the value of @method as a tag name.
	 *
	 * @param String the name of the method called
	 * @param Array arguments passed to this method
	 * @returns Mixed GXSimpleXMLElement on success, false on failure
	 */
	public function __call( $method, $args ){
		if( $method != __CLASS__ ){
			$tag = $method;
			$value = $args[0];
			$attributes = $args[1];
			$to = $args[2];
			if( !is_object($to) ){
				$to = $this->me;
			}
			for( $i = 0; $i < count($args); $i++ ){
				if( $args[$i] == CALL ){
					$callMethod = true;
				}
			}

			$validMethods = array(
				'xpath',
				'XPath',
				'replaceNode',
				'setStyleSheet',
				'xml',
				'XML',
				'saveXML',
				'asXML'
				);
			// if the method is one of the reserved function names AND we're NOT forcing it to be used as one...
			if( in_array($method,$validMethods) && $callMethod ){
				// handle reserved method names
				// can't use these in your xml file
				switch( $method ){
					case 'xpath':
					case 'XPATH':
					case 'XPath':
						return GXSimpleXMLTools::xpath( $this, $args[0] );
						break;
					case 'replaceNode':
						// @doc, @replaceWith, @toReplace, @xpath
						return GXSimpleXMLTools::replaceNode( $this->doc, $args[0], $args[1], $args[2] );
						break;
					case 'setStyleSheet':
						// @doc, @href
						return GXSimpleXMLTools::setStyleSheet( $this->doc, $args[0] );
						break;
					case 'xml':
					case 'XML':
					case 'saveXML':
					case 'asXML':
						// @doc
						return GXSimpleXMLTools::xml( $this->doc );
						break;
					default:
						return new GXSimpleXMLElement( $this->doc, $tag, $value, $attributes, $to );
				}
			}else{
				return new GXSimpleXMLElement( $this->doc, $tag, $value, $attributes, $to );
			}
		}
		trigger_error( 'Invalid method call in GXSimpleXML', E_USER_ERROR );
		return false;
	}

}

class GXSimpleXMLTools {

	static function xpath( &$node = null, $xpath = null ){
		if( !is_object($node) || !($xpath && is_string($xpath)) ){
			trigger_error( 'Missing information: node and xpath must both be present', E_USER_WARNING );
			return false;
		}
		// Exec xpath
		if( !($xresult = $node->xpath($xpath)) ){
			trigger_error( 'Unable to execute xpath query: ' .$xpath, E_USER_WARNING );
			return false;
		}
		return $xresult;
	}

	/*
	 * replaceNode
	 *
	 * Imports another doc, then replaces the specified node in this doc with it.
	 *
	 * @param Object the new TDOM object
	 * @param Object the node to be replaced
	 * @param String an xpath to be run on this document which identifies the node to be replaced
	 * @return Boolean
	 */
	static function replaceNode( &$doc, $replaceWith = null, $node = null, $xpath = null ){
		// Must have an object for replaceWith and either node or xpath
		if( !is_object($doc) || !is_object($replaceWith) || !($node || $xpath) ){
			trigger_error( 'Missing information: doc and replaceWith must be objects and either node or xpath is required', E_USER_ERROR );
			return false;
		}
		// If we have an xpath, set Node to the query results
		if( $xpath ){
			$tmp = $this->get( $xpath, null, 'dom' );
			$node = $tmp->item(0);
		}
		// Now perform the replacement
		$tmp = $doc->importNode( $replaceWith->doc->documentElement, true );
		if( !$node->parentNode ){
			trigger_error( 'parentNode of this node is invalid or missing, appending to this doc', E_USER_WARNING );
			$doc->documentElement->appendChild( $tmp );
		}else{
			$node->parentNode->replaceChild( $tmp, $node );
		}
		return true;
	}

	/*
	 * setStyleSheet
	 *
	 * Adds the xml-stylesheet processing instruction at the top of this document.
	 *
	 * @param String relative path to the XSL file
	 * @return boolean
	 */
	static function setStyleSheet( &$doc, $path = null ){
		if( !$path ){
			trigger_error( 'Missing information: path must be a string', E_USER_ERROR );
			return false;
		}
		$instr = $doc->createProcessingInstruction( 'xml-stylesheet', 'href="' .$path .'" type="text/xsl"' );
		$doc->insertBefore( $instr, $doc->documentElement );
		return true;
	}

	/*
     * xml
     *
     * Generates xml string
     *
     * @return string
     */
	static function xml( &$doc ){
		switch( get_class($doc) ){
			case 'GXSimpleXML':
				return $doc->doc->asXML();
				break;
			case 'GXSimpleXMLElement':
				return $doc->me->asXML();
				break;
			default:
				return $doc->asXML();
		}
		trigger_error( 'Can\'t generate xml from provided object: ' .get_class($doc), E_USER_ERROR );
		return false;
	}

	/*
	 * tag
	 *
	 * Returns a string valid for use as a node name or attribute name.
	 *
	 * @param String value to sanitize
	 * @return String new name
	 */
	static function cleanTag( $name = null ){
		if( !$name ){
			return false;
		}
		// If it's not a valid node name, sanitize it.
		$from = array('/','%','$','(',')','{','}','#');
		$tmp = trim( str_replace($from,'_',$name) );
		// If trimming it results in nothing, call it entry.
		if( !$tmp ){
			$tmp = 'entry';
		}
		// Names must begin with a letter. If it doesn't, prepend it with an underscore.
		if( preg_match( "/[^a-z]+/i", substr($tmp,0,1) ) && substr($tmp,0,1) != '_' ){
			debug( 'Key name (' .$name .') is not valid, returning _' .$tmp .' / ' .E_USER_ERROR );
			$tmp = '_' .$tmp;
		}
		return $tmp;
	}

	/*
	 * escapeText
	 *
	 * Returns values suitable for inclusion in a UTF-8 document.
	 * If the document will not be UTF-8, it just returns the value, untouched.
	 *
	 * @param String value to be escaped
	 * @return escaped value
	 */
	static function cleanText( $str = null ){
		if( !strlen($str) ){
			return false;
		}
		// Assume we're encoding to UTF-8, so escape MS Windows-1252 characters, then run iconv on it.
		// Note: your editor must be set to ISO-8859-1 or UTF-8, or the $map array below will no longer work.
		// You should see actual characters in every other key below.
		$map = array(
			"&"     => '&amp;',     // 2 'must be' escaped chars in xml
			"<"     => '&lt;',
			"•"     => '&#x2022;',  // bullet: ascii 149
			"\x95"  => '&#x2022;',
			" "     => '&#160;',    // non-breaking space: ascii 160
			"\xA0"  => '&#160;',
			"©"     => '&#169;',    // copyright: ascii 169
			"\xA9"  => '&#169;',
			"®"     => '&#174;',    // registered: ascii 174
			"\xAE"  => '&#174;',
			"™"     => '&#x2122;',	// trademark: ascii 153
			"\x99"  => '&#x2122;',
			"…"     => '&#x2026;',	// ellipsis: ascii 133
			"\x85"  => '&#x2026;',
			"‘"     => '&#x2018;',	// opening single quote: ascii 145
			"\x91"  => '&#x2018;',
			"’"     => '&#x2019;',	// closing single quote: ascii 146
			"\x92"  => '&#x2019;',
			"“"     => '&#x201C;',	// opening double quote: ascii 147
			"\x93"  => '&#x201C;',
			"”"     => '&#x201D;',	// closing double quote: ascii 148
			"\x94"  => '&#x201D;',
			"–"     => '&#x2013;',	// en dash: ascii 150
			"\x96"  => '&#x2013;',
			"—"     => '&#x2014;',	// em dash: ascii 151
			"\x97"  => '&#x2014;'
			);
		// First, run the translation for map.
		$temp = strtr( $str, $map );
		// Finally, run iconv to take care of any remaining encoding issues.
		return iconv( 'ISO-8859-1', 'UTF-8//TRANSLIT', $temp );
	}

}

?>